package com.studykun.client.ui;

import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import static com.studykun.client.util.IntegerStatics.*;

import java.util.HashMap;
import java.util.Map;

/**
 * A panel that lays its child widgets out "docked" at its outer edges, and
 * allows its last widget to take up the remaining space in its center.
 */
public class SkDockPanel extends ComplexPanel {
  private final Element eDockRoot_;
  
  private final HashMap<Widget, LayoutData> widgetLayoutData_ =
    new HashMap<Widget, LayoutData>();
  
  private Widget center_;

  static class LayoutData {
    public SkDockDirection direction_;

    public LayoutData(
        final SkDockDirection direction) {
      direction_ = direction;
    }
  }

  /**
   * Generate a debug ID for the {@link Widget} given the direction and number
   * of occurrences of the direction.
   * 
   * @param direction
   *  The direction of the widget.
   * @param count
   *  The number of widgets in that direction.
   */
  private static String generateDebugId(
      final SkDockDirection direction,
      final int count) {
    switch (direction) {
    case NORTH:
      return "north" + count;
    case SOUTH:
      return "south" + count;
    case WEST:
      return "west" + count;
    case EAST:
      return "east" + count; 
    default:  
      return "center";
    }
  }
  
  /**
   * Creates an empty dock panel.
   */
  public SkDockPanel() {
    eDockRoot_ = DOM.createDiv();
    
    DOM.setStyleAttribute(eDockRoot_, "position", "absolute");
    DOM.setStyleAttribute(eDockRoot_, "left", "0px");
    DOM.setStyleAttribute(eDockRoot_, "right", "0px");
    DOM.setStyleAttribute(eDockRoot_, "top", "0px");
    DOM.setStyleAttribute(eDockRoot_, "bottom", "0px");
    
    setElement(eDockRoot_);
  }

  /**
   * Adds a widget to the specified edge of the dock. If the widget is
   * already a child of this panel, this method behaves as though
   * {@link #remove(Widget)} had already been called.
   * 
   * @param widget
   *  The widget to be added.
   * @param direction
   *  The widget's direction in the dock.
   * 
   * @throws IllegalArgumentException
   *  When adding to the {@link #CENTER} and there is already a different
   *  widget there.
   */
  public void add(
      final Widget widget,
      final SkDockDirection direction) {
    // Validate
    if (direction == SkDockDirection.CENTER) {
      // Early out on the case of reinserting the center at the center.
      if (widget == center_) {
        return;
      } else if (center_ != null) {
        // Ensure a second 'center' widget is not being added.
        throw new IllegalArgumentException(
            "Only one CENTER widget may be added");
      }
    }

    // Detach new child.
    widget.removeFromParent();

    // Logical attach.
    getChildren().add(widget);
    if (direction == SkDockDirection.CENTER) {
      center_ = widget;
    }

    // Physical attach.
    final LayoutData layout = new LayoutData(direction);
    widgetLayoutData_.put(widget, layout);
    realizeTable();

    // Adopt.
    adopt(widget);
  }

  /**
   * Gets the layout direction of the given child widget.
   * 
   * @param widget
   *  The widget to be queried
   * @return
   *  The widget's layout direction, or <code>null</code> if it is not a child
   *  of this panel.
   */
  public SkDockDirection getWidgetDirection(
      final Widget widget) {
    if (widget.getParent() != this) {
      return null;
    }
    return widgetLayoutData_.get(widget).direction_;
  }

  @Override
  public boolean remove(
      final Widget widget) {
    final boolean removed = super.remove(widget);
    if (removed) {
      // Clear the center widget.
      if (widget == center_) {
        center_ = null;
      }
      if (widgetLayoutData_.containsKey(widget)) {
        widgetLayoutData_.remove(widget);
      }
      realizeTable();
    }
    return removed;
  }


  /**
   * {@link SkDockPanel} supports adding more than one cell in a direction, so an
   * integer will be appended to the end of the debug id. For example, the first
   * north cell is labeled "north1", the second is "north2", and the third is
   * "north3".
   * 
   * This widget recreates its structure every time a {@link Widget} is added,
   * so you must call this method after adding a new {@link Widget} or all debug
   * IDs will be lost.
   * 
   * <p>
   * <b>Affected Elements:</b>
   * <ul>
   * <li>-center = the center cell.</li>
   * <li>-north# = the northern cell.</li>
   * <li>-south# = the southern cell.</li>
   * <li>-east# = the eastern cell.</li>
   * <li>-west# = the western cell.</li>
   * </ul>
   * </p>
   * 
   * @see UIObject#onEnsureDebugId(String)
   */
  @Override
  protected void onEnsureDebugId(
      final String baseID) {
    super.onEnsureDebugId(baseID);

    final Map<SkDockDirection, Integer> directionCount =
      new HashMap<SkDockDirection, Integer>();
    
    for (final Widget child : getChildren()) {
      final SkDockDirection direction = widgetLayoutData_.get(child).direction_;

      // Get a debug id
      final int count = intValue(directionCount.get(direction), 1);
      final String debugID = generateDebugId(direction, count);
      ensureDebugId(DOM.getParent(child.getElement()), baseID, debugID);

      // Increment the count
      directionCount.put(direction, count + 1);
    }
  }

  /**
   * (Re)creates the DOM structure of the table representing the SkDockPanel,
   * based on the order and layout of the children.
   */
  private void realizeTable() {
    while (DOM.getChildCount(eDockRoot_) > 0) {
      DOM.removeChild(eDockRoot_, DOM.getChild(eDockRoot_, 0));
    }
    
    Element eDockParent = eDockRoot_; 

    for (final Widget child : getChildren()) {
      final SkDockDirection direction = widgetLayoutData_.get(child).direction_;
      
      if (direction == SkDockDirection.CENTER) {
        continue;
      }
      
      final Element eDockSide = DOM.createDiv();
      DOM.setStyleAttribute(eDockSide, "position", "absolute");
      
      final Element eDockHost = DOM.createDiv();
      
      final Element eDockRemainder = DOM.createDiv();
      DOM.setStyleAttribute(eDockRemainder, "position", "absolute");
      
      DOM.appendChild(eDockParent, eDockSide);
      DOM.appendChild(eDockSide, eDockHost);
      DOM.appendChild(eDockHost, child.getElement());
      
      DOM.appendChild(eDockParent, eDockRemainder);
      
      switch (direction) {
      case NORTH:
        DOM.setStyleAttribute(eDockSide, "top", "0px");
        DOM.setStyleAttribute(eDockSide, "height",
            DOM.getElementPropertyInt(eDockHost, "offsetHeight") + "px");
        DOM.setStyleAttribute(eDockSide, "left", "0px");
        DOM.setStyleAttribute(eDockSide, "right", "0px");

        DOM.setStyleAttribute(eDockRemainder, "top",
            DOM.getElementPropertyInt(eDockHost, "offsetHeight") + "px");
        DOM.setStyleAttribute(eDockRemainder, "bottom", "0px");
        DOM.setStyleAttribute(eDockRemainder, "left", "0px");
        DOM.setStyleAttribute(eDockRemainder, "right", "0px");
        break;
      case SOUTH:
        DOM.setStyleAttribute(eDockSide, "bottom", "0px");
        DOM.setStyleAttribute(eDockSide, "height",
            DOM.getElementPropertyInt(eDockHost, "offsetHeight") + "px");
        DOM.setStyleAttribute(eDockSide, "left", "0px");
        DOM.setStyleAttribute(eDockSide, "right", "0px");
        
        DOM.setStyleAttribute(eDockRemainder, "top", "0px");
        DOM.setStyleAttribute(eDockRemainder, "bottom",
            DOM.getElementPropertyInt(eDockHost, "offsetHeight") + "px");
        DOM.setStyleAttribute(eDockRemainder, "left", "0px");
        DOM.setStyleAttribute(eDockRemainder, "right", "0px");
        break;
      case EAST:
        DOM.setStyleAttribute(eDockSide, "right", "0px");
        DOM.setStyleAttribute(eDockSide, "width", child.getOffsetWidth() + "px");
        DOM.setStyleAttribute(eDockSide, "top", "0px");
        DOM.setStyleAttribute(eDockSide, "bottom", "0px");

        DOM.setStyleAttribute(eDockRemainder, "right", child.getOffsetWidth() + "px");
        DOM.setStyleAttribute(eDockRemainder, "left", "0px");
        DOM.setStyleAttribute(eDockRemainder, "top", "0px");
        DOM.setStyleAttribute(eDockRemainder, "bottom", "0px");
        break;
      case WEST:
        DOM.setStyleAttribute(eDockSide, "left", "0px");
        DOM.setStyleAttribute(eDockSide, "width", child.getOffsetWidth() + "px");
        DOM.setStyleAttribute(eDockSide, "top", "0px");
        DOM.setStyleAttribute(eDockSide, "bottom", "0px");

        DOM.setStyleAttribute(eDockRemainder, "right", "0px");
        DOM.setStyleAttribute(eDockRemainder, "left", child.getOffsetWidth() + "px");
        DOM.setStyleAttribute(eDockRemainder, "top", "0px");
        DOM.setStyleAttribute(eDockRemainder, "bottom", "0px");
        break;
      }

      DOM.setStyleAttribute(eDockHost, "position", "absolute");
      DOM.setStyleAttribute(eDockHost, "top", "0px");
      DOM.setStyleAttribute(eDockHost, "left", "0px");
      DOM.setStyleAttribute(eDockHost, "right", "0px");
      DOM.setStyleAttribute(eDockHost, "bottom", "0px");
      
      eDockParent = eDockRemainder;
    }
    
    if (center_ != null) {
      DOM.appendChild(eDockParent, center_.getElement());
    }
  }
}
